组件化是 Vue 的一大核心,接下来看看组件化的源码实现。

以下面例子进行分析:

<!-- App.vue -->
<template>
	<div id="app"></div>
</template>
// main.js
import Vue from 'vue';
import App from './App.vue';

new Vue({
  el: '#app',
  render: h => h(App)
});

new Vue进行实例化 Vue 之后,会执行 $mount 函数,实际上就是执行 mountComponent 函数,该函数会实例化一个渲染 watcher,在渲染 watcher 实例化过程中,会执行其传入的 updateComponent 函数。updateComponent 函数源码如下:

updateComponent = function () {
  vm._update(vm._render(), hydrating);
};
  • _render 函数:生成 vnode。
  • _update 函数:执行 patch 逻辑,根据 vnode 生成并挂在实际的 DOM。

# _render

由于 _render最后调用的是 _createElement 函数,直接分析该函数。上面例子由于 render 函数传入的是 vue 组件,所以在 _createElement 函数的执行逻辑为调用 createComponent 函数:

function _createElement (
 context,
 tag,
 data,
 children,
 normalizationType
) {
  // ...
  if (typeof tag === 'string') {
      // ...
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // 执行 createComponent 函数生成组件 vnode
      vnode = createComponent(Ctor, data, context, children, tag);
    } else {
      // ...
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children);
  }
  // ...
}

先来看生成组件 vnode 的 createComponent 函数源码逻辑。

# createComponent 函数

function createComponent (
  Ctor,
  data,
  context,
  children,
  tag
) {
  if (isUndef(Ctor)) {
    return
  }

  var baseCtor = context.$options._base;

  // 将 vue 组件的 options 转化为一个构造函数
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor);
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    {
      warn(("Invalid Component definition: " + (String(Ctor))), context);
    }
    return
  }

  // 异步组件处理逻辑
  var asyncFactory;
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor;
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {};

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor);

  // v-model 语法转化为 props 和 events 形式
  if (isDef(data.model)) {
    transformModel(Ctor.options, data);
  }

  // props 处理
  var propsData = extractPropsFromVNodeData(data, Ctor, tag);

  // 函数式组件处理
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  var listeners = data.on;
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn;

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    var slot = data.slot;
    data = {};
    if (slot) {
      data.slot = slot;
    }
  }

  // 安装组件钩子
  installComponentHooks(data);

  // 生成并返回一个占位的 vnode
  var name = Ctor.options.name || tag;
  var vnode = new VNode(
    ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
    data, undefined, undefined, undefined, context,
    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
    asyncFactory
  );

  return vnode
}

上面的源码,先着重看几个,其他的后面再看。

// 将 vue 组件的 options 转化为一个构造函数
if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor);
}
// 安装组件钩子
installComponentHooks(data);
// 生成并返回一个占位的 vnode
var name = Ctor.options.name || tag;
var vnode = new VNode(
  ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
  data, undefined, undefined, undefined, context,
  { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
  asyncFactory
);

# extend 函数

extend 函数在引入 vue 的时候就挂载到 vue 上了。实现逻辑在:initGlobalAPI 函数 -> initExtend 函数

function initExtend (Vue) {
  /**
   * Each instance constructor, including Vue, has a unique
   * cid. This enables us to create wrapped "child
   * constructors" for prototypal inheritance and cache them.
   */
  Vue.cid = 0;
  var cid = 1;

  /**
   * Class inheritance
   */
  Vue.extend = function (extendOptions) {
    extendOptions = extendOptions || {};
    var Super = this;
    var SuperId = Super.cid;
    var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    var name = extendOptions.name || Super.options.name;
    if (name) {
      validateComponentName(name);
    }

    // 定义组件构造器
    var Sub = function VueComponent (options) {
      this._init(options);
    };
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;
    Sub.cid = cid++;
    
    // 合并配置项目,使当前组件拥有全局配置
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    );
    Sub['super'] = Super;

    // 初始化 props
    if (Sub.options.props) {
      initProps$1(Sub);
    }
    // 初始化 computed
    if (Sub.options.computed) {
      initComputed$1(Sub);
    }

    // 继承静态方法
    Sub.extend = Super.extend;
    Sub.mixin = Super.mixin;
    Sub.use = Super.use;

    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type];
    });
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub;
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options;
    Sub.extendOptions = extendOptions;
    Sub.sealedOptions = extend({}, Sub.options);

    // 缓存构造器
    cachedCtors[SuperId] = Sub;
    return Sub
  };
}

该函数主要通过函数构造器创建 Vue 的子类,然后通过继承的方式将一个对象转化为一个继承于 Vue 的构造器 Sub 并返回。同时,对 Sub 本身也做了一些处理,如:props 和 computed 初始化,合并配置项目使当前组件拥有全局配置,继承静态方法等。

# installComponentHooks 函数

var componentVNodeHooks = {
  init: function init (vnode, hydrating) {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      var mountedNode = vnode; // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode);
    } else {
      var child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      );
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    }
  },
  prepatch: function prepatch (oldVnode, vnode) {
    // ...
  },
  insert: function insert (vnode) {
    // ...
  },
  destroy: function destroy (vnode) {
  	// ...
  }
}

var hooksToMerge = Object.keys(componentVNodeHooks);

function installComponentHooks (data) {
  var hooks = data.hook || (data.hook = {});
  for (var i = 0; i < hooksToMerge.length; i++) {
    var key = hooksToMerge[i];
    var existing = hooks[key];
    var toMerge = componentVNodeHooks[key];
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook$1(toMerge, existing) : toMerge;
    }
  }
}

function mergeHook$1 (f1, f2) {
  var merged = function (a, b) {
    // flow complains about extra args which is why we use any
    f1(a, b);
    f2(a, b);
  };
  merged._merged = true;
  return merged
}

该函数主要讲 componentVNodeHooks 的 hook 合并到 vm.data.hook 中,使每个 vue 实例的 vm.data.hook 能执行对应的钩子。

# _update

回到最开始的例子,在通过 createComponent 生成 App 的 vnode 之后,接下来就是通过 _update 函数进行 patch 的逻辑。由于 render 函数传入的是 vue 组件,所以执行逻辑为:

function patch(oldVnode, vnode, hydrating, removeOnly) {
  // ...
  if (isUndef(oldVnode)) {
    // 旧 vnode 为空,直接根据新 vnode 创建元素节点
    isInitialPatch = true;
    createElm(vnode, insertedVnodeQueue);
  }
  // ...
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
  return vnode.elm
}

function createElm (
	vnode,
 	insertedVnodeQueue,
 	parentElm,
 	refElm,
 	nested,
 	ownerArray,
 	index
) {
  // ...

  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
    
  // ...
}

# createComponent 函数

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  var i = vnode.data;
  if (isDef(i)) {
    var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
    // 调用 init hook
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */);
    }
    // 在调用了 init hook 之后,在上面例子中,由于 render 传入的是一个 组件,所以会调用 initComponent 将真实的 DOM 赋值给 vnode.elm,然后调用 insert 将 vnode.elm 插入到父节点中
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue);
      insert(parentElm, vnode.elm, refElm);
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
      }
      return true
    }
  }
}

该函数主要完成一下几件事:调用 init hook,创建组件实例;在执行完 init hook 之后,在最开始例子中,由于 render 传入的是一个 组件,所以会调用 initComponent 将真实的 DOM 赋值给 vnode.elm,最后调用 insert 将 vnode.elm 插入到父节点中。

# _init 组件钩子

var componentVNodeHooks = {
  init: function init (vnode, hydrating) {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // keep-alive 组件逻辑
      var mountedNode = vnode; // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode);
    } else {
      // 普通组件逻辑
      var child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      );
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    }
  }
}

init 函数主要有两个逻辑,一个是 keep-alive 组件,一个是普通组件。主要看普通组件逻辑,它会先执行 createComponentInstanceForVnode 函数创建组件实例并赋值给 vnode.componentInstance,然后执行 $mount 进行挂载,由于当前组件的 vnode 的 elm 为 空,所以 $mount 的第一个参数为 undefined。

# createComponentInstanceForVnode 函数

该函数有两个参数:

  • vnode:当前组件的 vnode
  • activeInstance:当前激活的 vue 实例。在我们例子中也就是当前 app 组件的父组件上下文 vm,也就是 new Vue

activeInstance 当前实例逻辑在 _update 最开始执行:

var activeInstance = null; // 全局变量 activeInstance
Vue.prototype._update = function (vnode, hydrating) {
    var vm = this;
    var prevEl = vm.$el;
    var prevVnode = vm._vnode;
    var restoreActiveInstance = setActiveInstance(vm);
  	vm._vnode = vnode;
		
  	// ...
}

function setActiveInstance(vm) {
  var prevActiveInstance = activeInstance;
  activeInstance = vm; // 将 activeInstance 设置为当前上下文 vm
  return function () {
    // 恢复上一个上下文
    activeInstance = prevActiveInstance;
  }
}

下面是 createComponentInstanceForVnode 函数源码:

function createComponentInstanceForVnode (
  // we know it's MountedComponentVNode but flow doesn't
  vnode,
  // activeInstance in lifecycle state
  parent
) {
  var options = {
    _isComponent: true,
    _parentVnode: vnode,
    parent: parent
  };
  // check inline-template render functions
  var inlineTemplate = vnode.data.inlineTemplate;
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render;
    options.staticRenderFns = inlineTemplate.staticRenderFns;
  }
  return new vnode.componentOptions.Ctor(options)
}

该函数先检查是否是内联 template render 函数,是的话执行对应逻辑。在最开始例子中,由于不是内联 template,所以直接执行并返回 new vnode.componentOptions.Ctor(options)。这个执行逻辑实际上就是执行:在 _render 时通过 createComponent 生成的组件构造器。

function initExtend (Vue) {
  /**
   * Each instance constructor, including Vue, has a unique
   * cid. This enables us to create wrapped "child
   * constructors" for prototypal inheritance and cache them.
   */
  Vue.cid = 0;
  var cid = 1;

  /**
   * Class inheritance
   */
  Vue.extend = function (extendOptions) {
    // ...

    // 定义组件构造器
    var Sub = function VueComponent (options) {
      this._init(options);
    };

    // ..
    return Sub
  };
}

接下来执行 _init 逻辑,下面是组件类型的执行逻辑源码:

function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    var vm = this;
    // a uid
    vm._uid = uid$3++;

    var startTag, endTag;
    /* istanbul ignore if */
    if (config.performance && mark) {
      startTag = "vue-perf-start:" + (vm._uid);
      endTag = "vue-perf-end:" + (vm._uid);
      mark(startTag);
    }

    // a flag to avoid this being observed
    vm._isVue = true;
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options);
    } else {
      // ...
    }
    /* istanbul ignore else */
    {
      initProxy(vm);
    }
    // expose real self
    vm._self = vm;
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, 'beforeCreate');
    initInjections(vm); // resolve injections before data/props
    initState(vm);
    initProvide(vm); // resolve provide after data/props
    callHook(vm, 'created');

    /* istanbul ignore if */
    if (config.performance && mark) {
      vm._name = formatComponentName(vm, false);
      mark(endTag);
      measure(("vue " + (vm._name) + " init"), startTag, endTag);
    }
		
    // 由于是子组件类型,el 为 undefined
    if (vm.$options.el) {
      // 这个挂载逻辑不会在这里执行
      vm.$mount(vm.$options.el);
    }
  };
}

主要关注两点:

  • initInternalComponent 函数:合并组件的配置项。
function initInternalComponent (vm, options) {
  var opts = vm.$options = Object.create(vm.constructor.options);
  // doing this because it's faster than dynamic enumeration.
  var parentVnode = options._parentVnode;
  opts.parent = options.parent;
  opts._parentVnode = parentVnode;

  var vnodeComponentOptions = parentVnode.componentOptions;
  opts.propsData = vnodeComponentOptions.propsData;
  opts._parentListeners = vnodeComponentOptions.listeners;
  opts._renderChildren = vnodeComponentOptions.children;
  opts._componentTag = vnodeComponentOptions.tag;

  if (options.render) {
    opts.render = options.render;
    opts.staticRenderFns = options.staticRenderFns;
  }
}
  • 由于是子组件类型,el 为 undefined,$mount 组件挂载逻辑不会在这里执行。

回到 componentVNodeHooks init 钩子,执行完 createComponentInstanceForVnode 函数生成组件实例后,会执行 $mount 进行挂载。

组件挂载的逻辑流程跟之前一样,生成渲染 watcher,然后通过 _render 函数生成 vnode,最后通过 _update 函数执行 patch 逻辑得到实际的 DOM 节点 vnode.elm 。在 patch 逻辑中,会执行到的主要源码如下:

function patch(oldVnode, vnode, hydrating, removeOnly) {
  // ...

  if (isUndef(oldVnode)) {
    // 旧 vnode 为空,直接根据新 vnode 创建元素节点
    isInitialPatch = true;
    createElm(vnode, insertedVnodeQueue);
  }

  // ...
  return vnode.elm
}

执行完 _update 之后,init hook 逻辑也就执行完了。在最开始例子中,接着会调用 initComponent 将真实的 DOM 赋值给 vnode.elm,最后调用 insert 将 vnode.elm 插入到父节点中。

initComponent 函数源码如下:

function initComponent(vnode, insertedVnodeQueue) {
    if (isDef(vnode.data.pendingInsert)) {
      insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
      vnode.data.pendingInsert = null;
    }
  	// 真实的 DOM 赋值给 vnode.elm
    vnode.elm = vnode.componentInstance.$el;
    if (isPatchable(vnode)) {
      // 执行在 patch 函数中初始化过的 create hook,存放在变量 cbs 中
      invokeCreateHooks(vnode, insertedVnodeQueue);
      setScope(vnode);
    } else {
      // empty component root.
      // skip all element-related modules except for ref (#3455)
      registerRef(vnode);
      // make sure to invoke the insert hook
      insertedVnodeQueue.push(vnode);
    }
  }

# 总结

Vue 组件化渲染的过程是一个深度优先遍历的过程。渲染过程如果遇到子组件会先将该子组件通过 _render 创建子组件构造器和安装组件钩子,然后通过 _update 创建子组件实例并得到组件的真实 DOM 节点,最后将真实的 DOM 挂载到其父节点上。